#!/usr/bin/env python3
############################################################################
# tools/kasan_global.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.  The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################

import argparse
import os

from construct import Int32ul, Int64ul, Struct
from elftools.elf.elffile import ELFFile

debug = False

# The maximum gap that two data segments can tolerate
KASAN_MAX_DATA_GAP = 1 << 16

# The section where the global variable descriptor
# generated by the compiler is located
KASAN_SECTION = ".kasan.global"

# Segments stored in the shadow area
KASAN_SHADOW_SECTION = ".kasan.shadows"

# The structure of parsing strings required for 32-bit and 64 bit
KASAN_GLOBAL_STRUCT_32 = Struct(
    "beg" / Int32ul,
    "size" / Int32ul,
    "size_with_redzone" / Int32ul,
    "name" / Int32ul,
    "module_name" / Int32ul,
    "has_dynamic_init" / Int32ul,
    "location" / Int32ul,
    "odr_indicator" / Int32ul,
)

KASAN_GLOBAL_STRUCT_64 = Struct(
    "beg" / Int64ul,
    "size" / Int64ul,
    "size_with_redzone" / Int64ul,
    "name" / Int64ul,
    "module_name" / Int64ul,
    "has_dynamic_init" / Int64ul,
    "location" / Int64ul,
    "odr_indicator" / Int64ul,
)


# Global configuration information
class Config:
    def __init__(self, outpath, elf, ldscript, align):
        self.outpath = outpath
        self.elf = elf
        self.ldscript = ldscript
        self.align = align

        if self.elf is None or os.path.exists(self.elf) is False:
            self.elf = None
            return

        with open(self.elf, "rb") as file:
            elf_file = ELFFile(file)
            elf_header = elf_file.header

            # Obtain bit width
            bitness = elf_header["e_ident"]["EI_CLASS"]
            if bitness == "ELFCLASS32":
                self.bitwides = 32
            elif bitness == "ELFCLASS64":
                self.bitwides = 64

            # Big and little end
            endianness = elf_header["e_ident"]["EI_DATA"]
            if endianness == "ELFDATA2LSB":
                self.endian = "little"
            elif endianness == "ELFDATA2MSB":
                self.endian = "big"


class KASanRegion:
    def __init__(self, start, end, align, endian, bitwides) -> None:
        self.start = start
        self.end = end
        self.align = align
        self.endian = endian
        self.bitwides = bitwides
        self.size = int((end - start) // self.align // self.bitwides) + 1
        self.__shadow = [0] * self.size

    def shadow(self) -> bytearray:
        ret = bytearray()
        for i in self.__shadow:
            for j in range(self.bitwides // 8):
                if self.endian == "little":
                    ret.append((i >> (j * 8)) & 0xFF)
                else:
                    ret.append((i >> ((self.bitwides // 8 - j - 1) * 8)) & 0xFF)
        return ret

    def mark_bit(self, index, nbit, nbits):
        for i in range(nbits):
            self.__shadow[index] |= 1 << nbit
            nbit += 1
            if nbit == self.bitwides:
                index += 1
                nbit = 0

    def poison(self, dict):
        dict_size = dict["size"]
        if dict_size % self.align:
            dict_size = int((dict_size + self.align - 1) // self.align) * self.align

        distance = (dict["beg"] + dict_size - self.start) // self.align

        # Index of the marked shadow area
        index = int(distance // self.bitwides)

        # The X-th bit of the specific byte marked
        nbit = distance % self.bitwides

        # Number of digits to be marked
        nbits = (dict["size_with_redzone"] - dict_size) // self.align

        if debug:
            print(
                "regin: %08x addr: %08x size: %d bits: %d || poison index: %d nbit: %d nbits: %d"
                % (
                    self.start,
                    dict["beg"],
                    dict["size"],
                    int(dict["size_with_redzone"] // self.align),
                    index,
                    nbit,
                    nbits,
                )
            )

        # Using 32bytes: with 1bit alignment,
        # only one bit of inaccessible area exists for each pair of global variables.
        self.mark_bit(index, nbit, nbits)


class KASanInfo:
    def __init__(self, align, endian, bitwides) -> None:
        self.align = align
        self.endian = endian
        self.bitwides = bitwides
        # Record the starting position of the merged data block
        self.data_sections = []
        # Record the kasan region corresponding to each data block
        self.regions: list[KASanRegion] = []

    def merge_ranges(self, dict):
        if len(self.data_sections) == 0:
            self.data_sections.append(
                [dict["beg"], dict["beg"] + dict["size_with_redzone"]]
            )
            return
        start = dict["beg"]
        end = dict["beg"] + dict["size_with_redzone"]
        if start - self.data_sections[-1][1] <= KASAN_MAX_DATA_GAP:
            self.data_sections[-1][1] = end
        else:
            self.data_sections.append([start, end])

    def create_region(self):
        for i in self.data_sections:
            start = i[0]
            end = i[1]
            if debug:
                print("KAsan Shadow Block: %08x ---- %08x" % (start, end))
            self.regions.append(
                KASanRegion(start, end, self.align, self.endian, self.bitwides)
            )

    def mark_shadow(self, dict):
        for i in self.regions:
            start = i.start
            end = i.end
            if start <= dict["beg"] and dict["beg"] + dict["size_with_redzone"] <= end:
                i.poison(dict)
                break


# Global variable descriptor
def get_global_dict(GLOBAL_STRUCT: Struct, bytes: bytes):
    dict = GLOBAL_STRUCT.parse(bytes)
    return {
        "beg": dict.beg,
        "size": dict.size,
        "size_with_redzone": dict.size_with_redzone,
    }


def get_elf_section(elf, section) -> bytes:
    with open(elf, "rb") as file:
        elf = ELFFile(file)
        for i in elf.iter_sections():
            if i.name == section:
                return i.data()
    return None


def long_to_bytestring(bitwides, endian, value: int) -> str:
    res = ""
    byte_array = value.to_bytes(length=int(bitwides / 8), byteorder=endian)
    for i in byte_array:
        res += "0x%02x, " % (i)
    return res


def create_kasan_file(config: Config, region_list=[]):
    region: KASanRegion = None
    with open(config.outpath, "w") as file:

        file.write("#include <nuttx/nuttx.h>\n")

        # Write the kasan region array
        for i in range(len(region_list)):
            file.write(
                'static const unsigned char locate_data("%s")'
                "\nglobals_region%d[] = {\n" % (KASAN_SHADOW_SECTION, i)
            )
            region = region_list[i]

            # Fill the array of regions
            # The filling order is as follows, from mm/kasan/generic.c
            # This type will be used to record the size of the shadow area
            # to facilitate the program to traverse the array.
            # 2. uintptr_t                  begin;
            # 3. uintptr_t                  end;
            # 4. uintptr_t                  shadow[1];

            file.write(
                "%s\n"
                % (long_to_bytestring(config.bitwides, config.endian, region.start))
            )
            file.write(
                "%s\n"
                % (long_to_bytestring(config.bitwides, config.endian, region.end))
            )

            shadow = region.shadow()
            for j in range(len(shadow)):
                if j % 8 == 0:
                    file.write("\n")
                file.write("0x%02x, " % (shadow[j]))

            file.write("\n};")

        # Record kasan region pointer location
        file.write("struct kasan_global_region_s;\n")
        file.write("const struct kasan_global_region_s *g_global_region[] = {\n")
        for i in range(len(region_list)):
            file.write(
                "\n(const struct kasan_global_region_s *)&globals_region%d," % (i)
            )
        file.write("0x00\n};")


# Error extraction section processing to enable the program to compile successfully
def handle_error(config: Config, string=None):
    if string:
        print(string)
    create_kasan_file(config)
    exit(0)


def parse_args() -> Config:
    global debug
    parser = argparse.ArgumentParser(description="Build kasan global variable region")
    parser.add_argument("-o", "--outpath", help="outpath")
    parser.add_argument("-a", "--align", default=32, type=int, help="alignment")
    parser.add_argument("-d", "--ldscript", help="ld script path(Only support sim)")
    parser.add_argument("-e", "--elffile", help="elffile")
    parser.add_argument(
        "--debug",
        action="store_true",
        default=False,
        help="if enabled, it will show more logs.",
    )

    args = parser.parse_args()
    debug = args.debug
    return Config(args.outpath, args.elffile, args.ldscript, args.align)


def main():

    config = parse_args()
    if config.elf is None:
        handle_error(config)

    # Identify the segment information that needs to be extracted
    section = get_elf_section(config.elf, KASAN_SECTION)
    if section is None:
        handle_error(
            config,
            "Please update the link script, section ['%s'] cannot be found"
            % (KASAN_SECTION),
        )

    # List of global variable descriptors
    dict_list = []

    # Extract all global variable descriptors within the
    if config.bitwides == 32:
        global_struct = KASAN_GLOBAL_STRUCT_32
    elif config.bitwides == 64:
        global_struct = KASAN_GLOBAL_STRUCT_64

    step = global_struct.sizeof()
    for i in range(0, len(section), step):
        dict = get_global_dict(global_struct, section[i : i + step])
        dict_list.append(dict)

    dict_list = sorted(dict_list, key=lambda item: item["beg"])

    # Merge all global variables to obtain several segments of the distribution range
    kasan = KASanInfo(config.align, config.endian, config.bitwides)
    for i in dict_list:
        kasan.merge_ranges(i)

    # Create empty shadow zone
    kasan.create_region()

    # Mark on the shadow area
    for i in dict_list:
        kasan.mark_shadow(i)

    create_kasan_file(config, kasan.regions)


if __name__ == "__main__":
    main()
